# -*- coding: utf-8 -*-
"""
This module gives abstract classes of mathematical objects.
"""

import math

class LineError(Exception):
    pass

def vectorFromTwoPoints(p1, p2):
    """Return the p1->p2 vector."""
    return Vector(
        p2['x']-p1['x'],
        p2['y']-p1['y']
    )

def lineFromVectorPoint(v, p):
    """
    Return the line which have v as director vector and which
    crosses 'point'.
    """
    return Line(
        v['y'],
        -v['x'],
        v['x']*p['y'] - v['y']*p['x']
    )

def lineFromTwoPoints(p1, p2):
    """Return the line which crosses p1 and p2."""
    return Line(
        p1['y'] - p2['y'],
        p2['x'] - p1['x'],
        p1['x']*p2['y'] - p2['x']*p1['y']
    )

def lineBissector(p1, p2, p3):
    """Return the bissector of the angle p1p2p3."""
    l1 = lineFromTwoPoints(p1, p2)
    l2 = lineFromTwoPoints(p2, p3)
    r1 = math.sqrt(l1['a']**2+l1['b']**2)
    r2 = math.sqrt(l2['a']**2+l2['b']**2)
    return Line(
        l1['a']*r2 -l2['a']*r1,
        l1['b']*r2 -l2['b']*r1,
        l1['c']*r2 -l2['c']*r1
    )

class CoordinateSystem:
    """
    Class for using a two-dimensional Cartesian coordinate system, with
    origin changes and scalings.
    
    Defined by the coordinates of the news system origin, the horizontal
    scaling and the vertical scaling.
    
    Example:
        repere = CoordinateSystem(
            Coordinates(2, 3),
            2,
            5
        )
    """
    
    def __init__(self, o, x, y):
        self.o=o
        self.x=float(x)
        self.y=float(y)
    
    def __repr__(self):
        return u"<CoordinateSystem scalingX='%s' scalingY='%s'>\n\tOrigin : %s\n</CoordinateSystem>" % (self.x, self.y, self.o)
        
    def apply(self, point):
        """
        Return the 'point' coordinates in the new coordinate
        system.
        """
        return Point(
            self.o['x'] + self.x*point['x'],
            self.o['y'] + self.y*point['y']
        )
        
    def depply(self, p):
        """
        Return the 'point' coordinates in the parent coordinate
        system.
        """
        return Point(
            (p['x'] - self.o['x'])/self.x,
            (p['y'] - self.o['y'])/self.y
        )
        
    def scale(self, p):
        """
        Return the 'point' coordinates in the coordinate system
        without taking origin change into count.
        """
        
        return Point(
            p['x']*self.x,
            p['y']*self.y
        )

class Coordinates:
    """
    Base class for using zero-dimensional objects as a coordinates couple
    as a vector or a point.
    
    Examples:
        Coordinates() is the couple (0, 0)
        Coordinates(4, 5)    : (4, 5)
        Coordinates(1)        : (1, 0)
        Coordinates(x=2)    : (2, 0)
        Coordinates(y=3)    : (0, 3)
    
    To get/set only one coordinate as a number, use __getitem__/__setitem__:
        >>> c = Coordinates(1, 2),
        >>> c['x']
        1
        >>> c['y']
        2
    """
    
    def __init__(self, x=0, y=0):
        """See the class doc."""
        self.__coordinates={'x':float(x), 'y':float(y)}
    
    def __getitem__(self, key):
        return self.__coordinates[key]
    def __setitem__(self, key, n):
        self.__coordinates[key]=float(n)
        
    def __add__(self, c):
        return Vector(self['x']+c['x'], self['y']+c['y'])
    def __mul__(self, n):
        return Vector(self['x']*n, self['y']*n)
    
    def __repr__(self):
        return u"<Coordinates x='%s' y='%s' />" % (self['x'], self['y'])
    def __eq__(self, c):
        (self["x"] == c["x"]) & (self["y"] == c["y"])
    def __ne__(self, c):
        (self["x"] != c["x"]) | (self["y"] != c["y"])
        
    def copy(self):
        """Return an other object which has the same coordinates."""
        return self.__class__(self['x'], self['y'])

    def move(self, c):
        """Add the coordinates and set the result in 'self'."""
        self['x'] += c['x']
        self['y'] += c['y']
        return self


class Vector(Coordinates):
    """Class extending Coordinates with very vectors methodes."""

    def __repr__(self):
        return u"<Vector x='%s' y='%s' />" % (self['x'], self['y'])

    def norm(self):
        """Return THE norm."""
        return math.sqrt( self['x']**2 + self['y']**2 )
    
    def isColinear(self, v):
        """
        Return True if 'self' and 'vector' are colinear, False
        otherwise. Be careful with this method: the computer's number
        representation is such as the result isn't accurate with
        some float coordinates.
        """
        return self['x']*v['y'] == self['y']*v['x']
    
    def normalVector(self):
        """Return A normal vector."""
        return Vector(-self['y'], self['x'])
    
    def normalizedVector(self):
        """Return THE normalized vector."""
        norm = self.norm()
        return ( self['x']/norm, self['y']/norm )

    def rotation(self, a):
        """
        Return the image of 'self' by the rotation whose the center is
        O(0, 0) and the angle 'a'.
        """
        normalized_vector = self.normalizedVector()
        absolute_angle = 2*math.atan( normalized_vector[1]/(1+normalized_vector[0]) ) + a
        vector_norm = self.norm()
        return Vecteur(vecteur_norm*math.cos(absolute), vecteur_norm*math.sin(absolute))

class Point(Coordinates):
    """Class extending Coordinates with very points methodes."""
    def __repr__(self):
        return u"<Point x='%s' y='%s' />" % (self['x'], self['y'])
    def pointAtRatio(self, p1, ratio):
        x = self['x'] + (ratio * (p1['x'] - self['x']))
        y = self['y'] + (ratio * (p1['y'] - self['y']))
        return Point(x, y)
    def pointAxialSymmetry(self, l): 
        """Return the symmetrical point of p relatively to l."""
        return self + vectorFromTwoPoints(
            self,
            l.perpendicularLine(self).intersection(l)
        )*2


class Line:
    """
    Geometric straight line.
    
    Line(a, b, c) represents the line which cartesian equation is
    1*a + 2*b + c = 0 
    
    a, b and c are reals, but a and b have not to be zero in the same time.
    However c is, if Line(0, 0, c) is tried, LineError will be
    raised.
    """

    def __init__(self, a, b, c):
        """See the class doc."""
        self.__numbers={'a':float(a), 'b':float(b), 'c':float(c)}
        self.__checkNumbers()
        
    def __checkNumbers(self):
        """
        Check if a and b aren't both zero. Raise LineError if they
        are.
        """
        if self['a']==self['b']==0: raise LineError, \
            u"a and b have not to be zero in the same time."
    def __getitem__(self, key):
        return self.__numbers[key]
    def __setitem__(self, key, number):
        self.__numbers[key]=float(number)
        self.__checkNumbers()
    def __repr__(self):
        return u"<Line equation=\"%s*x + %s*y + %s = 0\" />" % (self['a'], self['b'], self['c'])
    def directorVector(self):
        """Return A 'self' director vector."""
        return Vector(-self['b'], self['a'])
    def parallelLine(self, p):
        """Return THE parallel line to 'self' crossing 'point'."""
        return linefromVectorPoint(self.directorVector(), p)
    def perpendicularLine(self, p):
        """Return THE perpendicular line to 'self' crossing 'point'."""
        return lineFromVectorPoint(self.directorVector().normalVector(), p)
    def orthogonalProjectionOfPoint(self, p):
        """Return orthogonal projection of point p on self"""
        return self.intersection(self.perpendicularLine(p))
    def intersection(self, l):
        """
        Return the intersection of 'self' and 'l'. Raise
        LineError if 'self' and 'line' are parralel.
        """
        if self.directorVector().isColinear(l.directorVector()):
            raise LineError, u"Both lines are parallel."
        elif (self['c']==0) & (self['c']==0): return Coordinates(0,0)
        elif (self['a']!=0) & (self['b']!=0) & (l['a']!=0) & (l['b']!=0):
            y = (l['a']*self['c'] - self['a']*l['c']) / \
                (self['a']*l['b'] - l['a']*self['b'])
            return Point(-(self['b']*y+self['c'])/self['a'], y)
        else:
            if (l['a']==0) | (l['b']==0):
                d1, d2 = l, self
            else:
                d1, d2 = self, l
            if d1['a']==0:
                y=-float(d1['c'])/d1['b']
                return Point(-float(d2['b']*y+d2['c'])/d2['a'], y)
            if d1['b']==0:
                x=-float(d1['c'])/d1['a']
                return Point(x, -float(d2['a']*x+d2['c'])/d2['b'])
    def sectrice(self, l, a):
        """
        Return the line crossing the intersection of 'self' and 'line'
        passing through 'self' with the angle 'a'.
        """
        return self.parallelLine(self.directorVector().rotation(a), self.intersection(l))